<?xml version="1.0" encoding="utf-8" standalone="no"?>
<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.1//EN"
  "http://www.w3.org/TR/xhtml11/DTD/xhtml11.dtd">

<html xmlns="http://www.w3.org/1999/xhtml" xml:lang="zh-CN">
<head>
  <title></title>
  <link href="../Styles/stylesheet.css" rel="stylesheet" type="text/css" />
  
<style type="text/css">
@page { margin-bottom: 5.000000pt; margin-top: 5.000000pt; }
</style>
</head>

<body>
  <h2><span style="border-bottom:1px solid">Chapter_27</span></h2>

  <p>(int i = 0; i &lt; all.length; i++)</p>

  <p>if(!all[i].isAlive())</p>

  <p>all[i].start();</p>

  <p>// Suspends &amp; Stops all threads in</p>

  <p>// this group and its subgroups:</p>

  <p>System.out.println("All threads started");</p>

  <p>sys.suspend(); // Deprecated in Java 1.2</p>

  <p>// Never gets here...</p>

  <p>System.out.println("All threads suspended");</p>

  <p>sys.stop(); // Deprecated in Java 1.2</p>

  <p>System.out.println("All threads stopped");</p>

  <p>}</p>

  <p>} ///:~</p>

  <p>下面的输出结果已进行了适当的编辑，以便用一页能够装下（java.lang.已被删去），而且添加了适当的数字，与前面程序列表中括号里的数字对应：</p>

  <p>(1) ThreadGroup[name=system,maxpri=10]</p>

  <p>Thread[main,5,system]</p>

  <p>(2) ThreadGroup[name=system,maxpri=9]</p>

  <p>Thread[main,6,system]</p>

  <p>(3) ThreadGroup[name=g1,maxpri=9]</p>

  <p>Thread[A,9,g1]</p>

  <p>(4) ThreadGroup[name=g1,maxpri=8]</p>

  <p>Thread[A,9,g1]</p>

  <p>(5) ThreadGroup[name=g1,maxpri=8]</p>

  <p>Thread[A,9,g1]</p>

  <p>Thread[B,8,g1]</p>

  <p>(6) ThreadGroup[name=g1,maxpri=3]</p>

  <p>Thread[A,9,g1]</p>

  <p>Thread[B,8,g1]</p>

  <p>Thread[C,6,g1]</p>

  <p>(7) ThreadGroup[name=g1,maxpri=3]</p>

  <p>Thread[A,9,g1]</p>

  <p>Thread[B,8,g1]</p>

  <p>Thread[C,3,g1]</p>

  <p>(8) ThreadGroup[name=g2,maxpri=3]</p>

  <p>(9) ThreadGroup[name=g2,maxpri=3]</p>

  <p>(10)ThreadGroup[name=system,maxpri=9]</p>

  <p>Thread[main,6,system]</p>

  <p>ThreadGroup[name=g1,maxpri=3]</p>

  <p>Thread[A,9,g1]</p>

  <p>Thread[B,8,g1]</p>

  <p>Thread[C,3,g1]</p>

  <p>ThreadGroup[name=g2,maxpri=3]</p>

  <p>Thread[0,6,g2]</p>

  <p>Thread[1,6,g2]</p>

  <p>Thread[2,6,g2]</p>

  <p>Thread[3,6,g2]</p>

  <p>Thread[4,6,g2]</p>

  <p>Starting all threads:</p>

  <p>All threads started</p>

  <p>所有程序都至少有一个线程在运行，而且main()采取的第一项行动便是调用Thread的一个static（静态）方法，名为currentThread()。从这个线程开始，线程组将被创建，而且会为结果调用list()。输出如下：</p>

  <p>(1) ThreadGroup[name=system,maxpri=10]</p>

  <p>Thread[main,5,system]</p>

  <p>我们可以看到，主线程组的名字是system，而主线程的名字是main，而且它从属于system线程组。</p>

  <p>第二个练习显示出system组的最高优先级可以减少，而且main线程可以增大自己的优先级：</p>

  <p>(2) ThreadGroup[name=system,maxpri=9]</p>

  <p>Thread[main,6,system]</p>

  <p>第三个练习创建一个新的线程组，名为g1；它自动从属于system线程组，因为并没有明确指定它的归属关系。我们在g1内部放置了一个新线程，名为A。随后，我们试着将这个组的最大优先级设到最高的级别，并将A的优先级也设到最高一级。结果如下：</p>

  <p>(3) ThreadGroup[name=g1,maxpri=9]</p>

  <p>Thread[A,9,g1]</p>

  <p>可以看出，不可能将线程组的最大优先级设为高于它的父线程组。</p>

  <p>第四个练习将g1的最大优先级降低两级，然后试着把它升至Thread.MAX_PRIORITY。结果如下：</p>

  <p>(4) ThreadGroup[name=g1,maxpri=8]</p>

  <p>Thread[A,9,g1]</p>

  <p>同样可以看出，提高最大优先级的企图是失败的。我们只能降低一个线程组的最大优先级，而不能提高它。此外，注意线程A的优先级并未改变，而且它现在高于线程组的最大优先级。也就是说，线程组最大优先级的变化并不能对现有线程造成影响。</p>

  <p>第五个练习试着将一个新线程设为最大优先级。如下所示：</p>

  <p>(5) ThreadGroup[name=g1,maxpri=8]</p>

  <p>Thread[A,9,g1]</p>

  <p>Thread[B,8,g1]</p>

  <p>因此，新线程不能变到比最大线程组优先级还要高的一级。</p>

  <p>这个程序的默认线程优先级是6；若新建一个线程，那就是它的默认优先级，而且不会发生变化，除非对优先级进行了特别的处理。练习六将把线程组的最大优先级降至默认线程优先级以下，看看在这种情况下新建一个线程会发生什么事情：</p>

  <p>(6) ThreadGroup[name=g1,maxpri=3]</p>

  <p>Thread[A,9,g1]</p>

  <p>Thread[B,8,g1]</p>

  <p>Thread[C,6,g1]</p>

  <p>尽管线程组现在的最大优先级是3，但仍然用默认优先级6来创建新线程。所以，线程组的最大优先级不会影响默认优先级（事实上，似乎没有办法可以设置新线程的默认优先级）。</p>

  <p>改变了优先级后，接下来试试将其降低一级，结果如下：</p>

  <p>(7) ThreadGroup[name=g1,maxpri=3]</p>

  <p>Thread[A,9,g1]</p>

  <p>Thread[B,8,g1]</p>

  <p>Thread[C,3,g1]</p>

  <p>因此，只有在试图改变优先级的时候，才会强迫遵守线程组最大优先级的限制。</p>

  <p>我们在(8)和(9)中进行了类似的试验。在这里，我们创建了一个新的线程组，名为g2，将其作为g1的一个子组，并改变了它的最大优先级。大家可以看到，g2的优先级无论如何都不可能高于g1：</p>

  <p>(8) ThreadGroup[name=g2,maxpri=3]</p>

  <p>(9) ThreadGroup[name=g2,maxpri=3]</p>

  <p>也要注意在g2创建的时候，它会被自动设为g1的线程组最大优先级。</p>

  <p>经过所有这些实验以后，整个线程组和线程系统都会被打印出来，如下所示：</p>

  <p>(10)ThreadGroup[name=system,maxpri=9]</p>

  <p>Thread[main,6,system]</p>

  <p>ThreadGroup[name=g1,maxpri=3]</p>

  <p>Thread[A,9,g1]</p>

  <p>Thread[B,8,g1]</p>

  <p>Thread[C,3,g1]</p>

  <p>ThreadGroup[name=g2,maxpri=3]</p>

  <p>Thread[0,6,g2]</p>

  <p>Thread[1,6,g2]</p>

  <p>Thread[2,6,g2]</p>

  <p>Thread[3,6,g2]</p>

  <p>Thread[4,6,g2]</p>

  <p>所以由线程组的规则所限，一个子组的最大优先级在任何时候都只能低于或等于它的父组的最大优先级。</p>

  <p>本程序的最后一个部分演示了用于整组线程的方法。程序首先遍历整个线程树，并启动每一个尚未启动的线程。例如，system组随后会被挂起（暂停），最后被中止（尽管用suspend()和stop()对整个线程组进行操作看起来似乎很有趣，但应注意这些方法在Java 1.2里都是被“反对”的）。但在挂起system组的同时，也挂起了main线程，而且整个程序都会关闭。所以永远不会达到让线程中止的那一步。实际上，假如真的中止了main线程，它会“掷”出一个ThreadDeath违例，所以我们通常不这样做。由于ThreadGroup是从Object继承的，其中包含了wait()方法，所以也能调用wait(秒数×1000)，令程序暂停运行任意秒数的时间。当然，事前必须在一个同步块里取得对象锁。</p>

  <p>ThreadGroup类也提供了suspend()和resume()方法，所以能中止和启动整个线程组和它的所有线程，也能中止和启动它的子组，所有这些只需一个命令即可（再次提醒，suspend()和resume()都是Java 1.2所“反对”的）。</p>

  <p>从表面看，线程组似乎有些让人摸不着头脑，但请注意我们很少需要直接使用它们。</p>

  <p>14.5 回顾runnable</p>

  <p>在本章早些时候，我曾建议大家在将一个程序片或主Frame当作Runnable的实现形式之前，一定要好好地想一想。若采用那种方式，就只能在自己的程序中使用其中的一个线程。这便限制了灵活性，一旦需要用到属于那种类型的多个线程，就会遇到不必要的麻烦。</p>

  <p>当然，如果必须从一个类继承，而且想使类具有线程处理能力，则Runnable是一种正确的方案。本章最后一个例子对这一点进行了剖析，制作了一个RunnableCanvas类，用于为自己描绘不同的颜色（Canvas是“画布”的意思）。这个应用被设计成从命令行获得参数值，以决定颜色网格有多大，以及颜色发生变化之间的sleep()有多长。通过运用这些值，大家能体验到线程一些有趣而且可能令人费解的特性：</p>

  <p>//: ColorBoxes.java</p>

  <p>// Using the Runnable interface</p>

  <p>import java.awt.*;</p>

  <p>import java.awt.event.*;</p>

  <p>class CBox extends Canvas implements Runnable {</p>

  <p>private Thread t;</p>

  <p>private int pause;</p>

  <p>private static final Color[] colors = {</p>

  <p>Color.black, Color.blue, Color.cyan,</p>

  <p>Color.darkGray, Color.gray, Color.green,</p>

  <p>Color.lightGray, Color.magenta,</p>

  <p>Color.orange, Color.pink, Color.red,</p>

  <p>Color.white, Color.yellow</p>

  <p>};</p>

  <p>private Color cColor = newColor();</p>

  <p>private static final Color newColor() {</p>

  <p>return colors[</p>

  <p>(int)(Math.random() * colors.length)</p>

  <p>];</p>

  <p>}</p>

  <p>public void paint(Graphics g) {</p>

  <p>g.setColor(cColor);</p>

  <p>Dimension s = getSize();</p>

  <p>g.fillRect(0, 0, s.width, s.height);</p>

  <p>}</p>

  <p>public CBox(int pause) {</p>

  <p>this.pause = pause;</p>

  <p>t = new Thread(this);</p>

  <p>t.start();</p>

  <p>}</p>

  <p>public void run() {</p>

  <p>while(true) {</p>

  <p>cColor = newColor();</p>

  <p>repaint();</p>

  <p>try {</p>

  <p>t.sleep(pause);</p>

  <p>} catch(InterruptedException e) {}</p>

  <p>}</p>

  <p>}</p>

  <p>}</p>

  <p>public class ColorBoxes extends Frame {</p>

  <p>public ColorBoxes(int pause, int grid) {</p>

  <p>setTitle("ColorBoxes");</p>

  <p>setLayout(new GridLayout(grid, grid));</p>

  <p>for (int i = 0; i &lt; grid * grid; i++)</p>

  <p>add(new CBox(pause));</p>

  <p>addWindowListener(new WindowAdapter() {</p>

  <p>public void windowClosing(WindowEvent e) {</p>

  <p>System.exit(0);</p>

  <p>}</p>

  <p>});</p>

  <p>}</p>

  <p>public static void main(String[] args) {</p>

  <p>int pause = 50;</p>

  <p>int grid = 8;</p>

  <p>if(args.length &gt; 0)</p>

  <p>pause = Integer.parseInt(args[0]);</p>

  <p>if(args.length &gt; 1)</p>

  <p>grid = Integer.parseInt(args[1]);</p>

  <p>Frame f = new ColorBoxes(pause, grid);</p>

  <p>f.setSize(500, 400);</p>

  <p>f.setVisible(true);</p>

  <p>}</p>

  <p>} ///:~</p>

  <p>ColorBoxes是一个典型的应用（程序），有一个构建器用于设置GUI。这个构建器采用int grid的一个参数，用它设置GridLayout（网格布局），使每一维里都有一个grid单元。随后，它添加适当数量的CBox对象，用它们填充网格，并为每一个都传递pause值。在main()中，我们可看到如何对pause和grid的默认值进行修改（如果用命令行参数传递）。</p>

  <p>CBox是进行正式工作的地方。它是从Canvas继承的，并实现了Runnable接口，使每个Canvas也能是一个Thread。记住在实现Runnable的时候，并没有实际产生一个Thread对象，只是一个拥有run()方法的类。因此，我们必须明确地创建一个Thread对象，并将Runnable对象传递给构建器，随后调用start()（在构建器里进行）。在CBox里，这个线程的名字叫作t。</p>

  <p>请留意数组colors，它对Color类中的所有颜色进行了列举（枚举）。它在newColor()中用于产生一种随机选择的颜色。当前的单元（格）颜色是cColor。</p>

  <p>paint()则相当简单――只是将颜色设为cColor，然后用那种颜色填充整张画布（Canvas）。</p>

  <p>在run()中，我们看到一个无限循环，它将cColor设为一种随机颜色，然后调用repaint()把它显示出来。随后，对线程执行sleep()，使其“休眠”由命令行指定的时间长度。</p>

  <p>由于这种设计方案非常灵活，而且线程处理同每个Canvas元素都紧密结合在一起，所以在理论上可以生成任意多的线程（但在实际应用中，这要受到JVM能够从容对付的线程数量的限制）。</p>

  <p>这个程序也为我们提供了一个有趣的评测基准，因为它揭示了不同JVM机制在速度上造成的戏剧性的差异。</p>

  <p>14.5.1 过多的线程</p>

  <p>有些时候，我们会发现ColorBoxes几乎陷于停顿状态。在我自己的机器上，这一情况在产生了10×10的网格之后发生了。为什么会这样呢？自然地，我们有理由怀疑AWT对它做了什么事情。所以这里有一个例子能够测试那个猜测，它产生了较少的线程。代码经过了重新组织，使一个Vector实现了Runnable，而且那个Vector容纳了数量众多的色块，并随机挑选一些进行更新。随后，我们创建大量这些Vector对象，数量大致取决于我们挑选的网格维数。结果便是我们得到比色块少得多的线程。所以假如有一个速度的加快，我们就能立即知道，因为前例的线程数量太多了。如下所示：</p>

  <p>//: ColorBoxes2.java</p>

  <p>// Balancing thread use</p>

  <p>import java.awt.*;</p>

  <p>import java.awt.event.*;</p>

  <p>import java.util.*;</p>

  <p>class CBox2 extends Canvas {</p>

  <p>private static final Color[] colors = {</p>

  <p>Color.black, Color.blue, Color.cyan,</p>

  <p>Color.darkGray, Color.gray, Color.green,</p>

  <p>Color.lightGray, Color.magenta,</p>

  <p>Color.orange, Color.pink, Color.red,</p>

  <p>Color.white, Color.yellow</p>

  <p>};</p>

  <p>private Color cColor = newColor();</p>

  <p>private static final Color newColor() {</p>

  <p>return colors[</p>

  <p>(int)(Math.random() * colors.length)</p>

  <p>];</p>

  <p>}</p>

  <p>void nextColor() {</p>

  <p>cColor = newColor();</p>

  <p>repaint();</p>

  <p>}</p>

  <p>public void paint(Graphics g) {</p>

  <p>g.setColor(cColor);</p>

  <p>Dimension s = getSize();</p>

  <p>g.fillRect(0, 0, s.width, s.height);</p>

  <p>}</p>

  <p>}</p>

  <p>class CBoxVector</p>

  <p>extends Vector implements Runnable {</p>

  <p>private Thread t;</p>

  <p>private int pause;</p>

  <p>public CBoxVector(int pause) {</p>

  <p>this.pause = pause;</p>

  <p>t = new Thread(this);</p>

  <p>}</p>

  <p>public void go() { t.start(); }</p>

  <p>public void run() {</p>

  <p>while(true) {</p>

  <p>int i = (int)(Math.random() * size());</p>

  <p>((CBox2)elementAt(i)).nextColor();</p>

  <p>try {</p>

  <p>t.sleep(pause);</p>

  <p>} catch(InterruptedException e) {}</p>

  <p>}</p>

  <p>}</p>

  <p>}</p>

  <p>public class ColorBoxes2 extends Frame {</p>

  <p>private CBoxVector[] v;</p>

  <p>public ColorBoxes2(int pause, int grid) {</p>

  <p>setTitle("ColorBoxes2");</p>

  <p>setLayout(new GridLayout(grid, grid));</p>

  <p>v = new CBoxVector[grid];</p>

  <p>for(int i = 0; i &lt; grid; i++)</p>

  <p>v[i] = new CBoxVector(pause);</p>

  <p>for (int i = 0; i &lt; grid * grid; i++) {</p>

  <p>v[i % grid].addElement(new CBox2());</p>

  <p>add((CBox2)v[i % grid].lastElement());</p>

  <p>}</p>

  <p>for(int i = 0; i &lt; grid; i++)</p>

  <p>v[i].go();</p>

  <p>addWindowListener(new WindowAdapter() {</p>

  <p>public void windowClosing(WindowEvent e) {</p>

  <p>System.exit(0);</p>

  <p>}</p>

  <p>});</p>

  <p>}</p>

  <p>public static void main(String[] args) {</p>

  <p>// Shorter default pause than ColorBoxes:</p>

  <p>int pause = 5;</p>

  <p>int grid = 8;</p>

  <p>if(args.length &gt; 0)</p>

  <p>pause = Integer.parseInt(args[0]);</p>

  <p>if(args.length &gt; 1)</p>

  <p>grid = Integer.parseInt(args[1]);</p>

  <p>Frame f = new ColorBoxes2(pause, grid);</p>

  <p>f.setSize(500, 400);</p>

  <p>f.setVisible(true);</p>

  <p>}</p>

  <p>} ///:~</p>

  <p>在ColorBoxes2中，我们创建了CBoxVector的一个数组，并对其初始化，使其容下各个CBoxVector网格。每个网格都知道自己该“睡眠”多长的时间。随后为每个CBoxVector都添加等量的Cbox2对象，而且将每个Vector都告诉给go()，用它来启动自己的线程。</p>

  <p>CBox2类似CBox――能用一种随机选择的颜色描绘自己。但那就是CBox2能够做的全部工作。所有涉及线程的处理都已移至CBoxVector进行。</p>

  <p>CBoxVector也可以拥有继承的Thread，并有一个类型为Vector的成员对象。这样设计的好处就是addElement()和elementAt()方法可以获得特定的参数以及返回值类型，而不是只能获得常规Object（它们的名字也可以变得更短）。然而，这里采用的设计表面上看需要较少的代码。除此以外，它会自动保留一个Vector的其他所有行为。由于elementAt()需要大量进行“封闭”工作，用到许多括号，所以随着代码主体的扩充，最终仍有可能需要大量代码。</p>

  <p>和以前一样，在我们实现Runnable的时候，并没有获得与Thread配套提供的所有功能，所以必须创建一个新的Thread，并将自己传递给它的构建器，以便正式“启动”――start()――一些东西。大家在CBoxVector构建器和go()里都可以体会到这一点。run()方法简单地选择Vector里的一个随机元素编号，并为那个元素调用nextColor()，令其挑选一种新的随机颜色。</p>

  <p>运行这个程序时，大家会发现它确实变得更快，响应也更迅速（比如在中断它的时候，它能更快地停下来）。而且随着网格尺寸的壮大，它也不会经常性地陷于“停顿”状态。因此，线程的处理又多了一项新的考虑因素：必须随时检查自己有没有“太多的线程”（无论对什么程序和运行平台）。若线程太多，必须试着使用上面介绍的技术，对程序中的线程数量进行“平衡”。如果在一个多线程的程序中遇到了性能上的问题，那么现在有许多因素需要检查：</p>

  <p>(1) 对sleep，yield()以及／或者wait()的调用足够多吗？</p>

  <p>(2) sleep()的调用时间足够长吗？</p>

  <p>(3) 运行的线程数是不是太多？</p>

  <p>(4) 试过不同的平台和JVM吗？</p>

  <p>象这样的一些问题是造成多线程应用程序的编制成为一种“技术活”的原因之一。</p>

  <p>14.6 总结</p>

  <p>何时使用多线程技术，以及何时避免用它，这是我们需要掌握的重要课题。骼它的主要目的是对大量任务进行有序的管理。通过多个任务的混合使用，可以更有效地利用计算机资源，或者对用户来说显得更方便。资源均衡的经典问题是在IO等候期间如何利用CPU。至于用户方面的方便性，最经典的问题就是如何在一个长时间的下载过程中监视并灵敏地反应一个“停止”（stop）按钮的按下。</p>

  <p>多线程的主要缺点包括：</p>

  <p>(1) 等候使用共享资源时造成程序的运行速度变慢。</p>

  <p>(2) 对线程进行管理要求的额外CPU开销。</p>

  <p>(3) 复杂程度无意义的加大，比如用独立的线程来更新数组内每个元素的愚蠢主意。</p>

  <p>(4) 漫长的等待、浪费精力的资源竞争以及死锁等多线程症状。</p>

  <p>线程另一个优点是它们用“轻度”执行切换（100条指令的顺序）取代了“重度”进程场景切换（1000条指令）。由于一个进程内的所有线程共享相同的内存空间，所以“轻度”场景切换只改变程序的执行和本地变量。而在“重度”场景切换时，一个进程的改变要求必须完整地交换内存空间。</p>

  <p>线程处理看来好象进入了一个全新的领域，似乎要求我们学习一种全新的程序设计语言――或者至少学习一系列新的语言概念。由于大多数微机操作系统都提供了对线程的支持，所以程序设计语言或者库里也出现了对线程的扩展。不管在什么情况下，涉及线程的程序设计：</p>

  <p>(1) 刚开始会让人摸不着头脑，要求改换我们传统的编程思路；</p>

  <p>(2) 其他语言对线程的支持看来是类似的。所以一旦掌握了线程的概念，在其他环境也不会有太大的困难。尽管对线程的支持使Java语言的复杂程度多少有些增加，但请不要责怪Java。毕竟，利用线程可以做许多有益的事情。</p>

  <p>多个线程可能共享同一个资源（比如一个对象里的内存），这是运用线程时面临的最大的一个麻烦。必须保证多个线程不会同时试图读取和修改那个资源。这要求技巧性地运用synchronized（同步）关键字。它是一个有用的工具，但必须真正掌握它，因为假若操作不当，极易出现死锁。</p>

  <p>除此以外，运用线程时还要注意一个非常特殊的问题。由于根据Java的设计，它允许我们根据需要创建任意数量的线程――至少理论上如此（例如，假设为一项工程方面的有限元素分析创建数以百万的线程，这对Java来说并非实际）。然而，我们一般都要控制自己创建的线程数量的上限。因为在某些情况下，大量线程会将场面变得一团糟，所以工作都会几乎陷于停顿。临界点并不象对象那样可以达到几千个，而是在100以下。一般情况下，我们只创建少数几个关键线程，用它们解决某个特定的问题。这时数量的限制问题不大。但在较常规的一些设计中，这一限制确实会使我们感到束手束脚。</p>

  <p>大家要注意线程处理中一个不是十分直观的问题。由于采用了线程“调度”机制，所以通过在run()的主循环中插入对sleep()的调用，一般都可以使自己的程序运行得更快一些。这使它对编程技巧的要求非常高，特别是在更长的延迟似乎反而能提高性能的时候。当然，之所以会出现这种情况，是由于在正在运行的线程准备进入“休眠”状态之前，较短的延迟可能造成“sleep()结束”调度机制的中断。这便强迫调度机制将其中止，并于稍后重新启动，以便它能做完自己的事情，再进入休眠状态。必须多想一想，才能意识到事情真正的麻烦程度。</p>

  <p>本章遗漏的一件事情是一个动画例子，这是目前程序片最流行的一种应用。然而，Java JDK配套提供了解决这个问题的一整套方案（并可播放声音），大家可到java.sun.com的演示区域下载。此外，我们完全有理由相信未来版本的Java会提供更好的动画支持――尽管目前的Web涌现出了与传统方式完全不同的非Java、非程序化的许多动画方案。如果想系统学习Java动画的工作原理，可参考《Core Java――核心Java》一书，由Cornell&amp;Horstmann编著，Prentice-Hall于1997年出版。若欲更深入地了解线程处理，请参考《Concurrent Programming in Java――Java中的并发编程》，由Doug Lea编著，Addison-Wiseley于1997年出版；或者《Java Threads――Java线程》，Oaks&amp;Wong编著，O'Reilly于1997年出版。</p>

  <p>14.7 练习</p>

  <p>(1) 从Thread继承一个类，并（过载）覆盖run()方法。在run()内，打印出一条消息，然后调用sleep()。重复三遍这些操作，然后从run()返回。在构建器中放置一条启动消息，并覆盖finalize()，打印一条关闭消息。创建一个独立的线程类，使它在run()内调用System.gc()和System.runFinalization()，并打印一条消息，表明调用成功。创建这两种类型的几个线程，然后运行它们，看看会发生什么。</p>

  <p>(2) 修改Counter2.java，使线程成为一个内部类，而且不需要明确保存指向Counter2的一个。</p>

  <p>(3) 修改Sharing2.java，在TwoCounter的run()方法内部添加一个synchronized（同步）块，而不是同步整个run()方法。</p>

  <p>(4) 创建两个Thread子类，第一个的run()方法用于最开始的启动，并捕获第二个Thread对象的句柄，然后调用wait()。第二个类的run()应在过几秒后为第一个线程调用modifyAll()，使第一个线程能打印出一条消息。</p>

  <p>(5) 在Ticker2内的Counter5.java中，删除yield()，并解释一下结果。用一个sleep()换掉yield()，再解释一下结果。</p>

  <p>(6) 在ThreadGroup1.java中，将对sys.suspend()的调用换成对线程组的一个wait()调用，令其等候2秒钟。为了保证获得正确的结果，必须在一个同步块内取得sys的对象锁。</p>

  <p>(7) 修改Daemons.java，使main()有一个sleep()，而不是一个readLine()。实验不同的睡眠时间，看看会有什么发生。</p>

  <p>(8) 到第7章（中间部分）找到那个GreenhouseControls.java例子，它应该由三个文件构成。在Event.java中，Event类建立在对时间的监视基础上。修改这个Event，使其成为一个线程。然后修改其余的设计，使它们能与新的、以线程为基础的Event正常协作。</p>

  <p>英文版主页 | 中文版主页 | 详细目录 | 关于译者</p>

  <p>-------------------------------------------------</p>

  <p>TXT书库 www.16txt.com</p>

  <p>-------------------------------------------------第15章 网络编程</p>

  <p>历史上的网络编程都倾向于困难、复杂，而且极易出错。</p>

  <p>程序员必须掌握与网络有关的大量细节，有时甚至要对硬件有深刻的认识。一般地，我们需要理解连网协议中不同的“层”（Layer）。而且对于每个连网库，一般都包含了数量众多的函数，分别涉及信息块的连接、打包和拆包；这些块的来回运输；以及握手等等。这是一项令人痛苦的工作。</p>

  <p>但是，连网本身的概念并不是很难。我们想获得位于其他地方某台机器上的信息，并把它们移到这儿；或者相反。这与读写文件非常相似，只是文件存在于远程机器上，而且远程机器有权决定如何处理我们请求或者发送的数据。</p>

  <p>Java最出色的一个地方就是它的“无痛苦连网”概念。有关连网的基层细节已被尽可能地提取出去，并隐藏在JVM以及Java的本机安装系统里进行控制。我们使用的编程模型是一个文件的模型；事实上，网络连接（一个“套接字”）已被封装到系统对象里，所以可象对其他数据流那样采用同样的方法调用。除此以外，在我们处理另一个连网问题――同时控制多个网络连接――的时候，Java内建的多线程机制也是十分方便的。</p>

  <p>本章将用一系列易懂的例子解释Java的连网支持。</p>

  <p>15.1 机器的标识</p>

  <p>当然，为了分辨来自别处的一台机器，以及为了保证自己连接的是希望的那台机器，必须有一种机制能独一无二地标识出网络内的每台机器。早期网络只解决了如何在本地网络环境中为机器提供唯一的名字。但Java面向的是整个因特网，这要求用一种机制对来自世界各地的机器进行标识。为达到这个目的，我们采用了IP（互联网地址）的概念。IP以两种形式存在着：</p>

  <p>(1) 大家最熟悉的DNS（域名服务）形式。我自己的域名是bruceeckel.com。所以假定我在自己的域内有一台名为Opus的计算机，它的域名就可以是Opus.bruceeckel.com。这正是大家向其他人发送电子函件时采用的名字，而且通常集成到一个万维网（WWW）地址里。</p>

  <p>(2) 此外，亦可采用“四点”格式，亦即由点号（.）分隔的四组数字，比如202.98.32.111。</p>

  <p>不管哪种情况，IP地址在内部都表达成一个由32个二进制位（bit）构成的数字（注释①），所以IP地址的每一组数字都不能超过255。利用由java.net提供的static InetAddress.getByName()，我们可以让一个特定的Java对象表达上述任何一种形式的数字。结果是类型为InetAddress的一个对象，可用它构成一个“套接字”（Socket），大家在后面会见到这一点。</p>

  <p>①：这意味着最多只能得到40亿左右的数字组合，全世界的人很快就会把它用光。但根据目前正在研究的新IP编址方案，它将采用128 bit的数字，这样得到的唯一性IP地址也许在几百年的时间里都不会用完。</p>

  <p>作为运用InetAddress.getByName()一个简单的例子，请考虑假设自己有一家拨号连接因特网服务提供者（ISP），那么会发生什么情况。每次拨号连接的时候，都会分配得到一个临时IP地址。但在连接期间，那个IP地址拥有与因特网上其他IP地址一样的有效性。如果有人按照你的IP地址连接你的机器，他们就有可能使用在你机器上运行的Web或者FTP服务器程序。当然这有个前提，对方必须准确地知道你目前分配到的IP。由于每次拨号连接获得的IP都是随机的，怎样才能准确地掌握你的IP呢？</p>

  <p>下面这个程序利用InetAddress.getByName()来产生你的IP地址。为了让它运行起来，事先必须知道计算机的名字。该程序只在Windows 95中进行了测试，但大家可以依次进入自己的“开始”、“设置”、“控制面板”、“网络”，然后进入“标识”卡片。其中，“计算机名称”就是应在命令行输入的内容。//: WhoAmI.java</p>

  <p>// Finds out your network address when you're</p>

  <p>// connected to the Internet.</p>

  <p>package c15;</p>

  <p>import java.net.*;</p>

  <p>public class WhoAmI {</p>

  <p>public static void main(String[] args)</p>

  <p>throws Exception {</p>

  <p>if(args.length != 1) {</p>

  <p>System.err.println(</p>

  <p>"Usage: WhoAmI MachineName");</p>

  <p>System.exit(1);</p>

  <p>}</p>

  <p>InetAddress a =</p>

  <p>InetAddress.getByName(args[0]);</p>

  <p>System.out.println(a);</p>

  <p>}</p>

  <p>} ///:~</p>

  <p>就我自己的情况来说，机器的名字叫作“Colossus”（来自同名电影，“巨人”的意思。我在这台机器上有一个很大的硬盘）。所以一旦连通我的ISP，就象下面这样执行程序：</p>

  <p>java whoAmI Colossus</p>

  <p>得到的结果象下面这个样子（当然，这个地址可能每次都是不同的）：</p>

  <p>Colossus/202.98.41.151</p>

  <p>假如我把这个地址告诉一位朋友，他就可以立即登录到我的个人Web服务器，只需指定目标地址http://202.98.41.151即可（当然，我此时不能断线）。有些时候，这是向其他人发送信息或者在自己的Web站点正式出台以前进行测试的一种方便手段。</p>

  <p>15.1.1 服务器和客户机</p>

  <p>网络最基本的精神就是让两台机器连接到一起，并相互“交谈”或者“沟通”。一旦两台机器都发现了对方，就可以展开一次令人愉快的双向对话。但它们怎样才能“发现”对方呢？这就象在游乐园里那样：一台机器不得不停留在一个地方，侦听其他机器说：“嘿，你在哪里呢？”</p>

  <p>“停留在一个地方”的机器叫作“服务器”（Server）；到处“找人”的机器则叫作“客户机”（Client）或者“客户”。它们之间的区别只有在客户机试图同服务器连接的时候才显得非常明显。一旦连通，就变成了一种双向通信，谁来扮演服务器或者客户机便显得不那么重要了。</p>

  <p>所以服务器的主要任务是侦听建立连接的请求，这是由我们创建的特定服务器对象完成的。而客户机的任务是试着与一台服务器建立连接，这是由我们创建的特定客户机对象完成的。一旦连接建好，那么无论在服务器端还是客户机端，连接只是魔术般地变成了一个IO数据流对象。从这时开始，我们可以象读写一个普通的文件那样对待连接。所以一旦建好连接，我们只需象第10章那样使用自己熟悉的IO命令即可。这正是Java连网最方便的一个地方。</p>

  <p>1. 在没有网络的前提下测试程序</p>

  <p>由于多种潜在的原因，我们可能没有一台客户机、服务器以及一个网络来测试自己做好的程序。我们也许是在一个课堂环境中进行练习，或者写出的是一个不十分可靠的网络应用，还能拿到网络上去。IP的设计者注意到了这个问题，并建立了一个特殊的地址――localhost――来满足非网络环境中的测试要求。在Java中产生这个地址最一般的做法是：</p>

  <p>InetAddress addr = InetAddress.getByName(null);</p>

  <p>如果向getByName()传递一个null（空）值，就默认为使用localhost。我们用InetAddress对特定的机器进行索引，而且必须在进行进一步的操作之前得到这个InetAddress（互联网地址）。我们不可以操纵一个InetAddress的内容（但可把它打印出来，就象下一个例子要演示的那样）。创建InetAddress的唯一途径就是那个类的static（静态）成员方法getByName()（这是最常用的）、getAllByName()或者getLocalHost()。</p>

  <p>为得到本地主机地址，亦可向其直接传递字串"localhost"：</p>

  <p>InetAddress.getByName("localhost");</p>

  <p>或者使用它的保留IP地址（四点形式），就象下面这样：</p>

  <p>InetAddress.getByName("127.0.0.1");</p>

  <p>这三种方法得到的结果是一样的。</p>

  <p>15.1.2 端口：机器内独一无二的场所</p>

  <p>有些时候，一个IP地址并不足以完整标识一个服务器。这是由于在一台物理性的机器中，往往运行着多个服务器（程序）。由IP表达的每台机器也包含了“端口”（Port）。我们设置一个客户机或者服务器的时候，必须选择一个无论客户机还是服务器都认可连接的端口。就象我们去拜会某人时，IP地址是他居住的房子，而端口是他在的那个房间。</p>

  <p>注意端口并不是机器上一个物理上存在的场所，而是一种软件抽象（主要是为了表述的方便）。客户程序知道如何通过机器的IP地址同它连接，但怎样才能同自己真正需要的那种服务连接呢（一般每个端口都运行着一种服务，一台机器可能提供了多种服务，比如HTTP和FTP等等）？端口编号在这里扮演了重要的角色，它是必需的一种二级定址措施。也就是说，我们请求一个特定的端口，便相当于请求与那个端口编号关联的服务。“报时”便是服务的一个典型例子。通常，每个服务都同一台特定服务器机器上的一个独一无二的端口编号关联在一起。客户程序必须事先知道自己要求的那项服务的运行端口号。</p>

  <p>系统服务保留了使用端口1到端口1024的权力，所以不应让自己设计的服务占用这些以及其他任何已知正在使用的端口。本书的第一个例子将使用端口8080（为追忆我的第一台机器使用的老式8位Intel 8080芯片，那是一部使用CP/M操作系统的机子）。</p>

  <p>15.2 套接字</p>

  <p>“套接字”或者“插座”（Socket）也是一种软件形式的抽象，用于表达两台机器间一个连接的“终端”。针对一个特定的连接，每台机器上都有一个“套接字”，可以想象它们之间有一条虚拟的“线缆”。线缆的每一端都插入一个“套接字”或者“插座”里。当然，机器之间的物理性硬件以及电缆连接都是完全未知的。抽象的基本宗旨是让我们尽可能不必知道那些细节。</p>

  <p>在Java中，我们创建一个套接字，用它建立与其他机器的连接。从套接字得到的结果是一个InputStream以及OutputStream（若使用恰当的转换器，则分别是Reader和Writer），以便将连接作为一个IO流对象对待。有两个基于数据流的套接字类：ServerSocket，服务器用它“侦听”进入的连接；以及Socket，客户用它初始一次连接。一旦客户（程序）申请建立一个套接字连接，ServerSocket就会返回（通过accept()方法）一个对应的服务器端套接字，以便进行直接通信。从此时起，我们就得到了真正的“套接字－套接字”连接，可以用同样的方式对待连接的两端，因为它们本来就是相同的！此时可以利用getInputStream()以及getOutputStream()从每个套接字产生对应的InputStream和OutputStream对象。这些数据流必须封装到缓冲区内。可按第10章介绍的方法对类进行格式化，就象对待其他任何流对象那样。</p>

  <p>对于Java库的命名机制，ServerSocket（服务器套接字）的使用无疑是容易产生混淆的又一个例证。大家可能认为ServerSocket最好叫作“ServerConnector”（服务器连接器），或者其他什么名字，只是不要在其中安插一个“Socket”。也可能以为ServerSocket和Socket都应从一些通用的基础类继承。事实上，这两种类确实包含了几个通用的方法，但还不够资格把它们赋给一个通用的基础类。相反，ServerSocket的主要任务是在那里耐心地等候其他机器同它连接，再返回一个实际的Socket。这正是“ServerSocket”这个命名不恰当的地方，因为它的目标不是真的成为一个Socket，而是在其他人同它连接的时候产生一个Socket对象。</p>

  <p>然而，ServerSocket确实会在主机上创建一个物理性的“服务器”或者侦听用的套接字。这个套接字会侦听进入的连接，然后利用accept()方法返回一个“已建立”套接字（本地和远程端点均已定义）。容易混淆的地方是这两个套接字（侦听和已建立）都与相同的服务器套接字关联在一起。侦听套接字只能接收新的连接请求，不能接收实际的数据包。所以尽管ServerSocket对于编程并无太大的意义，但它确实是“物理性”的。</p>

  <p>创建一个ServerSocket时，只需为其赋予一个端口编号。不必把一个IP地址分配它，因为它已经在自己代表的那台机器上了。但在创建一个Socket时，却必须同时赋予IP地址以及要连接的端口编号（另一方面，从ServerSocket.accept()返回的Socket已经包含了所有这些信息）。</p>

  <p>15.2.1 一个简单的服务器和客户机程序</p>

  <p>这个例子将以最简单的方式运用套接字对服务器和客户机进行操作。服务器的全部工作就是等候建立一个连接，然后用那个连接产生的Socket创建一个InputStream以及一个OutputStream。在这之后，它从InputStream读入的所有东西都会反馈给OutputStream，直到接收到行中止（END）为止，最后关闭连接。</p>

  <p>客户机连接与服务器的连接，然后创建一个OutputStream。文本行通过OutputStream发送。客户机也会创建一个InputStream，用它收听服务器说些什么（本例只不过是反馈回来的同样的字句）。</p>

  <p>服务器与客户机（程序）都使用同样的端口号，而且客户机利用本地主机地址连接位于同一台机器中的服务器（程序），所以不必在一个物理性的网络里完成测试（在某些配置环境中，可能需要同真正的网络建立连接，否则程序不能工作――尽管实际并不通过那个网络通信）。</p>

  <p>下面是服务器程序：</p>

  <p>//: JabberServer.java</p>

  <p>// Very simple server that just</p>

  <p>// echoes whatever the client sends.</p>

  <p>import java.io.*;</p>

  <p>import java.net.*;</p>

  <p>public class JabberServer {</p>

  <p>// Choose a port outside of the range 1-1024:</p>

  <p>public static final int PORT = 8080;</p>

  <p>public static void main(String[] args)</p>

  <p>throws IOException {</p>

  <p>ServerSocket s = new ServerSocket(PORT);</p>

  <p>System.out.println("Started: " + s);</p>

  <p>try {</p>

  <p>// Blocks until a connection occurs:</p>

  <p>Socket socket = s.accept();</p>

  <p>try {</p>

  <p>System.out.println(</p>

  <p>"Connection accepted: "+ socket);</p>

  <p>BufferedReader in =</p>

  <p>new BufferedReader(</p>

  <p>new InputStreamReader(</p>

  <p>socket.getInputStream()));</p>

  <p>// Output is automatically flushed</p>

  <p>// by PrintWriter:</p>

  <p>PrintWriter out =</p>

  <p>new PrintWriter(</p>

  <p>new BufferedWriter(</p>

  <p>new OutputStreamWriter(</p>

  <p>socket.getOutputStream())),true);</p>

  <p>while (true) {</p>

  <p>String str = in.readLine();</p>

  <p>if (str.equals("END")) break;</p>

  <p>System.out.println("Echoing: " + str);</p>

  <p>out.println(str);</p>

  <p>}</p>

  <p>// Always close the two sockets...</p>

  <p>} finally {</p>

  <p>System.out.println("closing...");</p>

  <p>socket.close();</p>

  <p>}</p>

  <p>} finally {</p>

  <p>s.close();</p>

  <p>}</p>

  <p>}</p>

  <p>} ///:~</p>

  <p>可以看到，ServerSocket需要的只是一个端口编号，不需要IP地址（因为它就在这台机器上运行）。调用accept()时，方法会暂时陷入停顿状态（堵塞），直到某个客户尝试同它建立连接。换言之，尽管它在那里等候连接，但其他进程仍能正常运行（参考第14章）。建好一个连接以后，accept()就会返回一个Socket对象，它是那个连接的代表。</p>

  <p>清除套接字的责任在这里得到了很艺术的处理。假如ServerSocket构建器失败，则程序简单地退出（注意必须保证ServerSocket的构建器在失败之后不会留下任何打开的网络套接字）。针对这种情况，main()会“掷”出一个IOException违例，所以不必使用一个try块。若ServerSocket构建器成功执行，则其他所有方法调用都必须到一个try-finally代码块里寻求保护，以确保无论块以什么方式留下，ServerSocket都能正确地关闭。</p>

  <p>同样的道理也适用于由accept()返回的Socket。若accept()失败，那么我们必须保证Socket不再存在或者含有任何资源，以便不必清除它们。但假若执行成功，则后续的语句必须进入一个try-finally块内，以保障在它们失败的情况下，Socket仍能得到正确的清除。由于套接字使用了重要的非内存资源，所以在这里必须特别谨慎，必须自己动手将它们清除（Java中没有提供“破坏器”来帮助我们做这件事情）。</p>

  <p>无论ServerSocket还是由accept()产生的Socket都打印到System.out里。这意味着它们的toString方法会得到自动调用。这样便产生了：</p>

  <p>ServerSocket[addr=0.0.0.0,PORT=0,localport=8080]</p>

  <p>Socket[addr=127.0.0.1,PORT=1077,localport=8080]</p>

  <p>大家不久就会看到它们如何与客户程序做的事情配合。</p>

  <p>程序的下一部分看来似乎仅仅是打开文件，以便读取和写入，只是InputStream和OutputStream是从Socket对象创建的。利用两个“转换器”类InputStreamReader和OutputStreamWriter，InputStream和OutputStream对象已经分别转换成为Java 1.1的Reader和Writer对象。也可以直接使用Java1.0的InputStream和OutputStream类，但对输出来说，使用Writer方式具有明显的优势。这一优势是通过PrintWriter表现出来的，它有一个过载的构建器，能获取第二个参数――一个布尔值标志，指向是否在每一次println()结束的时候自动刷新输出（但不适用于print()语句）。每次写入了输出内容后（写进out），它的缓冲区必须刷新，使信息能正式通过网络传递出去。对目前这个例子来说，刷新显得尤为重要，因为客户和服务器在采取下一步操作之前都要等待一行文本内容的到达。若刷新没有发生，那么信息不会进入网络，除非缓冲区满（溢出），这会为本例带来许多问题。</p>

  <p>编写网络应用程序时，需要特别注意自动刷新机制的使用。每次刷新缓冲区时，必须创建和发出一个数据包（数据封）。就目前的情况来说，这正是我们所希望的，因为假如包内包含了还没有发出的文本行，服务器和客户机之间的相互“握手”就会停止。换句话说，一行的末尾就是一条消息的末尾。但在其他许多情况下，消息并不是用行分隔的，所以不如不用自动刷新机制，而用内建的缓冲区判决机制来决定何时发送一个数据包。这样一来，我们可以发出较大的数据包，而且处理进程也能加快。</p>

  <p>注意和我们打开的几乎所有数据流一样，它们都要进行缓冲处理。本章末尾有一个练习，清楚展现了假如我们不对数据流进行缓冲，那么会得到什么样的后果（速度会变慢）。</p>

  <p>无限while循环从BufferedReader in内读取文本行，并将信息写入System.out，然后写入PrintWriter.out。注意这可以是任何数据流，它们只是在表面上同网络连接。</p>

  <p>客户程序发出包含了"END"的行后，程序会中止循环，并关闭Socket。</p>

  <p>下面是客户程序的源码：</p>

  <p>//: JabberClient.java</p>

  <p>// Very simple client that just sends</p>

  <p>// lines to the server and reads lines</p>

  <p>// that the server sends.</p>

  <p>import java.net.*;</p>

  <p>import java.io.*;</p>

  <p>public class JabberClient {</p>

  <p>public static void main(String[] args)</p>

  <p>throws IOException {</p>

  <p>// Passing null to getByName() produces the</p>

  <p>// special "Local Loopback" IP address, for</p>

  <p>// testing on one machine w/o a network:</p>

  <p>InetAddress addr =</p>

  <p>InetAddress.getByName(null);</p>

  <p>// Alternatively, you can use</p>

  <p>// the address or name:</p>

  <p>// InetAddress addr =</p>

  <p>// InetAddress.getByName("127.0.0.1");</p>

  <p>// InetAddress addr =</p>

  <p>// InetAddress.getByName("localhost");</p>

  <p>System.out.println("addr = " + addr);</p>

  <p>Socket socket =</p>

  <p>new Socket(addr, JabberServer.PORT);</p>

  <p>// Guard everything in a try-finally to make</p>

  <p>// sure that the socket is closed:</p>

  <p>try {</p>

  <p>System.out.println("socket = " + socket);</p>

  <p>BufferedReader in =</p>

  <p>new BufferedReader(</p>

  <p>new InputStreamReader(</p>

  <p>socket.getInputStream()));</p>

  <p>// Output is automatically flushed</p>

  <p>// by PrintWriter:</p>

  <p>PrintWriter out =</p>

  <p>new PrintWriter(</p>

  <p>new BufferedWriter(</p>

  <p>new OutputStreamWriter(</p>

  <p>socket.getOutputStream())),true);</p>

  <p>for(int i = 0; i &lt; 10; i ++) {</p>

  <p>out.println("howdy " + i);</p>

  <p>String str = in.readLine();</p>

  <p>System.out.println(str);</p>

  <p>}</p>

  <p>out.println("END");</p>

  <p>} finally {</p>

  <p>System.out.println("closing...");</p>

  <p>socket.close();</p>

  <p>}</p>

  <p>}</p>

  <p>} ///:~</p>

  <p>在main()中，大家可看到获得本地主机IP地址的InetAddress的三种途径：使用null，使用localhost，或者直接使用保留地址127.0.0.1。当然，如果想通过网络同一台远程主机连接，也可以换用那台机器的IP地址。打印出InetAddress addr后（通过对toString()方法的自动调用），结果如下：</p>

  <p>localhost/127.0.0.1</p>

  <p>通过向getByName()传递一个null，它会默认寻找localhost，并生成特殊的保留地址127.0.0.1。注意在名为socket的套接字创建时，同时使用了InetAddress以及端口号。打印这样的某个Socket对象时，为了真正理解它的含义，请记住一次独一无二的因特网连接是用下述四种数据标识的：clientHost（客户主机）、clientPortNumber（客户端口号）、serverHost（服务主机）以及serverPortNumber（服务端口号）。服务程序启动后，会在本地主机（127.0.0.1）上建立为它分配的端口（8080）。一旦客户程序发出请求，机器上下一个可用的端口就会分配给它（这种情况下是1077），这一行动也在与服务程序相同的机器（127.0.0.1）上进行。现在，为了使数据能在客户及服务程序之间来回传送，每一端都需要知道把数据发到哪里。所以在同一个“已知”服务程序连接的时候，客户会发出一个“返回地址”，使服务器程序知道将自己的数据发到哪儿。我们在服务器端的示范输出中可以体会到这一情况：</p>

  <p>Socket[addr=127.0.0.1,port=1077,localport=8080]</p>

  <p>这意味着服务器刚才已接受了来自127.0.0.1这台机器的端口1077的连接，同时监听自己的本地端口（8080）。而在客户端：</p>

  <p>Socket[addr=localhost/127.0.0.1,PORT=8080,localport=1077]</p>

  <p>这意味着客户已用自己的本地端口1077与127.0.0.1机器上的端口8080建立了 连接。</p>

  <p>大家会注意到每次重新启动客户程序的时候，本地端口的编号都会增加。这个编号从1025（刚好在系统保留的1-1024之外）开始，并会一直增加下去，除非我们重启机器。若重新启动机器，端口号仍然会从1025开始增值（在Unix机器中，一旦超过保留的套按字范围，数字就会再次从最小的可用数字开始）。</p>

  <p>创建好Socket对象后，将其转换成BufferedReader和PrintWriter的过程便与在服务器中相同（同样地，两种情况下都要从一个Socket开始）。在这里，客户通过发出字串"howdy"，并在后面跟随一个数字，从而初始化通信。注意缓冲区必须再次刷新（这是自动发生的，通过传递给PrintWriter构建器的第二个参数）。若缓冲区没有刷新，那么整个会话（通信）都会被挂起，因为用于初始化的“howdy”永远不会发送出去（缓冲区不够满，不足以造成发送动作的自动进行）。从服务器返回的每一行都会写入System.out，以验证一切都在正常运转。为中止会话，需要发出一个"END"。若客户程序简单地挂起，那么服务器会“掷”出一个违例。</p>

  <p>大家在这里可以看到我们采用了同样的措施来确保由Socket代表的网络资源得到正确的清除，这是用一个try-finally块实现的。</p>

  <p>套接字建立了一个“专用”连接，它会一直持续到明确断开连接为止（专用连接也可能间接性地断开，前提是某一端或者中间的某条链路出现故障而崩溃）。这意味着参与连接的双方都被锁定在通信中，而且无论是否有数据传递，连接都会连续处于开放状态。从表面看，这似乎是一种合理的连网方式。然而，它也为网络带来了额外的开销。本章后面会介绍进行连网的另一种方式。采用那种方式，连接的建立只是暂时的。</p>

  <p>15.3 服务多个客户</p>

  <p>JabberServer可以正常工作，但每次只能为一个客户程序提供服务。在典型的服务器中，我们希望同时能处理多个客户的请求。解决这个问题的关键就是多线程处理机制。而对于那些本身不支持多线程的语言，达到这个要求无疑是异常困难的。通过第14章的学习，大家已经知道Java已对多线程的处理进行了尽可能的简化。由于Java的线程处理方式非常直接，所以让服务器控制多名客户并不是件难事。</p>

  <p>最基本的方法是在服务器（程序）里创建单个ServerSocket，并调用accept()来等候一个新连接。一旦accept()返回，我们就取得结果获得的Socket，并用它新建一个线程，令其只为那个特定的客户服务。然后再调用accept()，等候下一次新的连接请求。</p>

  <p>对于下面这段服务器代码，大家可发现它与JabberServer.java例子非常相似，只是为一个特定的客户提供服务的所有操作都已移入一个独立的线程类中：</p>

  <p>//: MultiJabberServer.java</p>

  <p>// A server that uses multithreading to handle</p>

  <p>// any number of clients.</p>

  <p>import java.io.*;</p>

  <p>import java.net.*;</p>

  <p>class ServeOneJabber extends Thread {</p>

  <p>private Socket socket;</p>

  <p>private BufferedReader in;</p>

  <p>private PrintWriter out;</p>

  <p>public ServeOneJabber(Socket s)</p>

  <p>throws IOException {</p>

  <p>socket = s;</p>

  <p>in =</p>

  <p>new BufferedReader(</p>

  <p>new InputStreamReader(</p>

  <p>socket.getInputStream()));</p>

  <p>// Enable auto-flush:</p>

  <p>out =</p>

  <p>new PrintWriter(</p>

  <p>new BufferedWriter(</p>

  <p>new OutputStreamWriter(</p>

  <p>socket.getOutputStream())), true);</p>

  <p>// If any of the above calls throw an</p>

  <p>// exception, the caller is responsible for</p>

  <p>// closing the socket. Otherwise the thread</p>

  <p>// will close it.</p>

  <p>start(); // Calls run()</p>

  <p>}</p>

  <p>public void run() {</p>

  <p>try {</p>

  <p>while (true) {</p>

  <p>String str = in.readLine();</p>

  <p>if (str.equals("END")) break;</p>

  <p>System.out.println("Echoing: " + str);</p>

  <p>out.println(str);</p>

  <p>}</p>

  <p>System.out.println("closing...");</p>

  <p>} catch (IOException e) {</p>

  <p>} finally {</p>

  <p>try {</p>

  <p>socket.close();</p>

  <p>} catch(IOException e) {}</p>

  <p>}</p>

  <p>}</p>

  <p>}</p>

  <p>public class MultiJabberServer {</p>

  <p>static final int PORT = 8080;</p>

  <p>public static void main(String[] args)</p>

  <p>throws IOException {</p>

  <p>ServerSocket s = new ServerSocket(PORT);</p>

  <p>System.out.println("Server Started");</p>

  <p>try {</p>

  <p>while(true) {</p>

  <p>// Blocks until a connection occurs:</p>

  <p>Socket socket = s.accept();</p>

  <p>try {</p>

  <p>new ServeOneJabber(socket);</p>

  <p>} catch(IOException e) {</p>

  <p>// If it fails, close the socket,</p>

  <p>// otherwise the thread will close it:</p>

  <p>socket.close();</p>

  <p>}</p>

  <p>}</p>

  <p>} finally {</p>

  <p>s.close();</p>

  <p>}</p>

  <p>}</p>

  <p>} ///:~</p>

  <p>每次有新客户请求建立一个连接时，ServeOneJabber线程都会取得由accept()在main()中生成的Socket对象。然后和往常一样，它创建一个BufferedReader，并用Socket自动刷新PrintWriter对象。最后，它调用Thread的特殊方法start()，令其进行线程的初始化，然后调用run()。这里采取的操作与前例是一样的：从套扫字读入某些东西，然后把它原样反馈回去，直到遇到一个特殊的"END"结束标志为止。</p>

  <p>同样地，套接字的清除必须进行谨慎的设计。就目前这种情况来说，套接字是在ServeOneJabber外部创建的，所以清除工作可以“共享”。若ServeOneJabber构建器失败，那么只需向调用者“掷”出一个违例即可，然后由调用者负责线程的清除。但假如构建器成功，那么必须由ServeOneJabber对象负责线程的清除，这是在它的run()里进行的。</p>

  <p>请注意MultiJabberServer有多么简单。和以前一样，我们创建一个ServerSocket，并调用accept()允许一个新连接的建立。但这一次，accept()的返回值（一个套接字）将传递给用于ServeOneJabber的构建器，由它创建一个新线程，并对那个连接进行控制。连接中断后，线程便可简单地消失。</p>

  <p>如果ServerSocket创建失败，则再一次通过main()掷出违例。如果成功，则位于外层的try-finally代码块可以担保正确的清除。位于内层的try-catch块只负责防范ServeOneJabber构建器的失败；若构建器成功，则ServeOneJabber线程会将对应的套接字关掉。</p>

  <p>为了证实服务器代码确实能为多名客户提供服务，下面这个程序将创建许多客户（使用线程），并同相同的服务器建立连接。每个线程的“存在时间”都是有限的。一旦到期，就留出空间以便创建一个新线程。允许创建的线程的最大数量是由final int maxthreads决定的。大家会注意到这个值非常关键，因为假如把它设得很大，线程便有可能耗尽资源，并产生不可预知的程序错误。</p>

  <p>//: MultiJabberClient.java</p>

  <p>// Client that tests the MultiJabberServer</p>

  <p>// by starting up multiple clients.</p>

  <p>import java.net.*;</p>

  <p>import java.io.*;</p>

  <p>class JabberClientThread extends Thread {</p>

  <p>private Socket socket;</p>

  <p>private BufferedReader in;</p>

  <p>private PrintWriter out;</p>

  <p>private static int counter = 0;</p>

  <p>private int id = counter++;</p>

  <p>private static int threadcount = 0;</p>

  <p>public static int threadCount() {</p>

  <p>return threadcount;</p>

  <p>}</p>

  <p>public JabberClientThread(InetAddress addr) {</p>

  <p>System.out.println("Making client " + id);</p>

  <p>threadcount++;</p>

  <p>try {</p>

  <p>socket =</p>

  <p>new Socket(addr, MultiJabberServer.PORT);</p>

  <p>} catch(IOException e) {</p>

  <p>// If the creation of the socket fails,</p>

  <p>// nothing needs to be cleaned up.</p>

  <p>}</p>

  <p>try {</p>

  <p>in =</p>

  <p>new BufferedReader(</p>

  <p>new InputStreamReader(</p>

  <p>socket.getInputStream()));</p>

  <p>// Enable auto-flush:</p>

  <p>out =</p>

  <p>new PrintWriter(</p>

  <p>new BufferedWriter(</p>

  <p>new OutputStreamWriter(</p>

  <p>socket.getOutputStream())), true);</p>

  <p>start();</p>

  <p>} catch(IOException e) {</p>

  <p>// The socket should be closed on any</p>

  <p>// failures other than the socket</p>

  <p>// constructor:</p>

  <p>try {</p>

  <p>socket.close();</p>

  <p>} catch(IOException e2) {}</p>

  <p>}</p>

  <p>// Otherwise the socket will be closed by</p>

  <p>// the run() method of the thread.</p>

  <p>}</p>

  <p>public void run() {</p>

  <p>try {</p>

  <p>for(int i = 0; i &lt; 25; i++) {</p>

  <p>out.println("Client " + id + ": " + i);</p>

  <p>String str = in.readLine();</p>

  <p>System.out.println(str);</p>

  <p>}</p>

  <p>out.println("END");</p>

  <p>} catch(IOException e) {</p>

  <p>} finally {</p>

  <p>// Always close it:</p>

  <p>try {</p>

  <p>socket.close();</p>

  <p>} catch(IOException e) {}</p>

  <p>threadcount--; // Ending this thread</p>

  <p>}</p>

  <p>}</p>

  <p>}</p>

  <p>public class MultiJabbe</p>

  <div class="mbppagebreak"></div>
</body>
</html>
